/* @flow */

import config from '../config'
import { warn } from './debug'
import { nativeWatch } from './env'
import { set } from '../observer/index'

import {
  ASSET_TYPES,
  LIFECYCLE_HOOKS
} from 'shared/constants'

import {
  extend,
  hasOwn,
  camelize,
  capitalize,
  isBuiltInTag,
  isPlainObject
} from 'shared/util'

/**
 * Option overwriting strategies are functions that handle
 * how to merge a parent option value and a child option
 * value into the final value.
 */
const
  strats = config.optionMergeStrategies

/**
 * Options with restrictions
 */
if (process.env.NODE_ENV !== 'production') {
  strats.el = strats.propsData = function (parent, child, vm, key) {
    //什么情况下vm实例不存在？
    //通过extend或者mixin方法设置Vue构造函数及其子构造函数的options选项对象时，
    //不会存在vm实例。也就是说，不可以为extend和mixin方法设置el和propsData属性
    //首先，需要搞清楚vm实例是什么，构造函数又是什么。vm实例是通过构造函数创建的。
    //构造函数中的选项必须是它所创建的所有vm实例都可通用的选项，而通过new Vue(options)方式
    //传入的options选项对象是只与当前Vue实例关联的。通过extend和mixin方法都是向构造函数中添加选项，
    //而对于el和propsData选项只是针对特定Vue实例的，所以只可通过new Vue(options)的方式提供
    //而不能通过extend或mixin方法添加到构造函数选项中。比如对于el选项，如果添加都构造函数的选项中，
    //则其创建的所有Vue实例都会处理该el元素，这显然是不正确的。
    if (!vm) {
      warn(
        `option "${key}" can only be used during instance ` +
        'creation with the `new` keyword.'
      )
    }
    return defaultStrat(parent, child)
  }
}

/**
 * Helper that recursively merges two data objects together.
 * 该方法深度遍历from对象，只有在该对象中的某个属性在to对象中不存在时，
 * 才会将该属性设置到to对象上。对于属性名相同的属性，会进行深拷贝。
 */
function mergeData (to: Object, from: ?Object): Object {
  if (!from) return to
  let key, toVal, fromVal
  const keys = Object.keys(from)
  for (let i = 0; i < keys.length; i++) {
    key = keys[i]
    toVal = to[key]
    fromVal = from[key]
    //对于from对象中的属性，只有在to中不存在时，
    //才会将该属性以及对应的值设置到to中。
    if (!hasOwn(to, key)) {
      set(to, key, fromVal)
    } else if (isPlainObject(toVal) && isPlainObject(fromVal)) { //注意，这里是深拷贝
      mergeData(toVal, fromVal)
    }
  }
  return to
}

/**
 * Data
 * 该方法返回一个函数，该返回函数的运行结果是parentVal和childVal合并后的data对象
 * 为什么无论是在vm实例是否存在，都需要在合并data属性时返回一个函数
 * 答：因为可能会存在多个data需要合并为一个data对象，所以返回一个函数就是在柯利化，
 * 用函数记录这多个data，并且如果是为组件合并data对象，则其值必须为函数，所以统一
 * 都设置为函数
 */
export function mergeDataOrFn (
  parentVal: any,
  childVal: any,
  vm?: Component
): ?Function {
  if (!vm) { //进入if，则说明vm未定义，也就是通过Vue.extend合并选项对象
    // in a Vue.extend merge, both should be functions
    if (!childVal) {
      return parentVal
    }
    if (!parentVal) {
      return childVal
    }
    // when parentVal & childVal are both present,
    // we need to return a function that returns the
    // merged result of both functions... no need to
    // check if parentVal is a function here because
    // it has to be a function to pass previous merges.
    return function mergedDataFn () {
      return mergeData(
        typeof childVal === 'function' ? childVal.call(this) : childVal,
        parentVal.call(this) //parentVal
      )
    }
  } else if (parentVal || childVal) {
    return function mergedInstanceDataFn () {
      // instance merge
      const instanceData = typeof childVal === 'function'
        ? childVal.call(vm)
        : childVal
      const defaultData = typeof parentVal === 'function'
        ? parentVal.call(vm)
        : undefined
      if (instanceData) {
        return mergeData(instanceData, defaultData)
      } else {
        return defaultData
      }
    }
  }
}

strats.data = function (
  parentVal: any,
  childVal: any,
  vm?: Component
): ?Function {
  //为什么Vue实例不存在，data属性的值就必须是函数？
  //答：首先要明白，何时vm参数有值，何时vm参数无值。
  //1）无值：在通过extend合并或者mixin获得构造函数的选项对象时，vm参数是无值的。此时，因为是在创建构造函数的选项对象，所以
  //  这个选项对象会在所有的构造函数创建的实例之间共享的，因此，该选项对象的data属性必须是函数，以保证为每一个Vue实例返回不同的data值。
  //2）有值：在通过new Vue({...})创建实例时，vm参数是有值的。此时，合并的选项对象只是当前vue实例私有的，所以无需保证
  //  childVal必须为函数。
  if (!vm) {
    if (childVal && typeof childVal !== 'function') {
      process.env.NODE_ENV !== 'production' && warn(
        'The "data" option should be a function ' +
        'that returns a per-instance value in component ' +
        'definitions.',
        vm
      )

      return parentVal
    }
    return mergeDataOrFn.call(this, parentVal, childVal)
  }

  return mergeDataOrFn(parentVal, childVal, vm)
}

/**
 * Hooks and props are merged as arrays.
 * 将parentVal与childVal合并为一个数组，parentVal中的钩子函数在
 * childVal之前执行
 */
function mergeHook (
  parentVal: ?Array<Function>,
  childVal: ?Function | ?Array<Function>
): ?Array<Function> {
  return childVal
    ? parentVal
      ? parentVal.concat(childVal)
      : Array.isArray(childVal)
        ? childVal
        : [childVal]
    : parentVal
}

//生命周期钩子方法
LIFECYCLE_HOOKS.forEach(hook => {
  strats[hook] = mergeHook
})

/**
 * Assets
 *
 * When a vm is present (instance creation), we need to do
 * a three-way merge between constructor options, instance
 * options and parent options.
 * childVal对象中的属性会覆盖parentVal中的属性，但是不会直接修改parentVal对象，
 * 而是通过原型继承的方法完成的。
 */
function mergeAssets (parentVal: ?Object, childVal: ?Object): Object {
  //创建一个原型对象是parentVal的对象res，则在对象res上做的修改
  //不会影响到parentVal对象。
  const res = Object.create(parentVal || null)
  return childVal
    ? extend(res, childVal)
    : res
}

/**
 * component、directive、filter
 */
ASSET_TYPES.forEach(function (type) {
  strats[type + 's'] = mergeAssets
})

/**
 * Watchers.
 *
 * Watchers hashes should not overwrite one
 * another, so we merge them as arrays.
 */
strats.watch = function (parentVal: ?Object, childVal: ?Object): ?Object {
  // work around Firefox's Object.prototype.watch...
  if (parentVal === nativeWatch) parentVal = undefined
  if (childVal === nativeWatch) childVal = undefined
  /* istanbul ignore if */
  if (!childVal) return Object.create(parentVal || null)
  if (!parentVal) return childVal
  const ret = {}
  extend(ret, parentVal)
  for (const key in childVal) {
    let parent = ret[key]
    const child = childVal[key]
    if (parent && !Array.isArray(parent)) {
      parent = [parent]
    }
    ret[key] = parent
      ? parent.concat(child)
      : Array.isArray(child) ? child : [child]
  }
  return ret
}

/**
 * Other object hashes.
 */
strats.props =
strats.methods =
strats.inject =
strats.computed = function (parentVal: ?Object, childVal: ?Object): ?Object {
  if (!childVal) return Object.create(parentVal || null)
  if (!parentVal) return childVal
  const ret = Object.create(null)
  extend(ret, parentVal)
  extend(ret, childVal)
  return ret
}
strats.provide = mergeDataOrFn

/**
 * Default strategy.
 */
const defaultStrat = function (parentVal: any, childVal: any): any {
  return childVal === undefined
    ? parentVal
    : childVal
}

/**
 * Validate component names
 */
function checkComponents (options: Object) {
  for (const key in options.components) {
    const lower = key.toLowerCase()
    // isBuiltInTag方法用于判断是否是Vue内置的组件，比如Component。
    // config.isReservedTag方法用于判断是否为原生的HTML标签
    if (isBuiltInTag(lower) || config.isReservedTag(lower)) {
      warn(
        'Do not use built-in or reserved HTML elements as component ' +
        'id: ' + key
      )
    }
  }
}

/**
 * Ensure all props option syntax are normalized into the
 * Object-based format.
 * 总结： 该方法用于修正options中的props属性，props可以是数组或者对象类型，如果是数组，则内部元素必须全是字符串类型，并且数组类型最终
 * 会被转换为对象类型，其中type为null。对象类型的键的值如果是连字符形式，也会被转换为驼峰式，所以声明props属性时，不要使用连字符
 * 形式，而要使用驼峰式。
 * props对象的值的形式可为：
 * 1）字符串数组：['hello', 'world']，内部数据必须全部为字符串，被转化为{hello:{type:null}, world:{type:null}}
 * 2）对象形式一：{hello:String, world:String}，被转化为{hello:{type:String}, world:{type:String}}
 * 3）对象形式二：{hello:{type:String, 'default':'go'}, world:{type:String}}
 */
function normalizeProps (options: Object) {
  //获得最原始的props对象，接下来会对该对象进行修正
  const props = options.props
  if (!props) return
  const res = {}
  let i, val, name

  //props是数组类型
  if (Array.isArray(props)) {
    i = props.length
    while (i--) {
      val = props[i]
      if (typeof val === 'string') {
        name = camelize(val)
        res[name] = { type: null }
      } else if (process.env.NODE_ENV !== 'production') {
        warn('props must be strings when using array syntax.')
      }
    }
  } else if (isPlainObject(props)) {
    for (const key in props) {
      val = props[key]
      name = camelize(key)
      res[name] = isPlainObject(val)
        ? val
        : { type: val }
    }
  }
  //使用修正后的props覆盖原props
  options.props = res
}

/**
 * Normalize all injections into Object-based format
 * 依赖注入的inject选项的修正。inject可以为数组或对象类型，
 * 如果inject选项为对象类型，则是提供对注入的对象名称进行重命名的功能，其中键为在当前组件中
 * 使用的命名，而值为在可用的注入中查找的索引值。
 */
function normalizeInject (options: Object) {
  const inject = options.inject
  if (Array.isArray(inject)) {
    const normalized = options.inject = {}
    for (let i = 0; i < inject.length; i++) {
      normalized[inject[i]] = inject[i]
    }
  }
}

/**
 * Normalize raw function directives into object format.
 * 修正指令集directives属性
 */
function normalizeDirectives (options: Object) {
  const dirs = options.directives
  if (dirs) {
    for (const key in dirs) {
      const def = dirs[key]
      if (typeof def === 'function') {
        dirs[key] = { bind: def, update: def }
      }
    }
  }
}

/**
 * Merge two option objects into a new one.
 * Core utility used in both instantiation and inheritance.
 * 该方法用于合并parent, child中的属性。首先会修正child中的相关属性，比如props等。
 * 然后调用属性相关的合并策略合并子父的属性。而vm参数会传入合并策略函数中。
 * child的优先级比parent高，所以相应的合并策略可查看有道云笔记。
 */
export function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component
): Object {
  if (process.env.NODE_ENV !== 'production') {
    checkComponents(child)
  }

  //什么时候child会是函数？
  //当child为函数时，该函数是Vue或其子构造函数，主要为了可以直接设置extend方法的参数为Vue或其子构造函数，
  //
  //例如：
  //let One = Vue.extend({
  //  props:{
  //    one: String
  //  }
  //});
  //
  //let Two = Vue.extend({
  //  props:{
  //    two: String
  //  }
  //});
  //
  //let Result = One.extend(Two);
  //这里Result将同时继承了One和Two的特性。
  //
  //当然在使用new Vue()的方式提供选项对象，或者使用extends、mixins时，也可如此使用，但是感觉这样使用不太合理，
  //比较合理的使用方式就是通过extend方法。
  if (typeof child === 'function') {
    //如果child为函数，则会获得其options属性对应的对象作为child选项对象，
    //由此可见，该child函数应该就是Vue或其子构造函数
    child = child.options
  }

  normalizeProps(child)
  normalizeInject(child)
  normalizeDirectives(child)
  //先解析extends，后解析mixins，所以extends的props等选项中的同名属性会被mixins的同名属性覆盖，
  //但是extends中的钩子函数等会在mixins的钩子函数之前运行。
  const extendsFrom = child.extends
  if (extendsFrom) {
    parent = mergeOptions(parent, extendsFrom, vm)
  }
  if (child.mixins) {
    for (let i = 0, l = child.mixins.length; i < l; i++) {
      parent = mergeOptions(parent, child.mixins[i], vm)
    }
  }
  const options = {}
  let key
  for (key in parent) {
    mergeField(key)
  }
  for (key in child) {
    if (!hasOwn(parent, key)) {
      mergeField(key)
    }
  }
  function mergeField (key) {
    // 合并策略
    const strat = strats[key] || defaultStrat
    options[key] = strat(parent[key], child[key], vm, key)
  }
  return options
}

/**
 * Resolve an asset.
 * This function is used because child instances need access
 * to assets defined in its ancestor chain.
 */
export function resolveAsset (
  options: Object,
  type: string,
  id: string,
  warnMissing?: boolean
): any {
  /* istanbul ignore if */
  if (typeof id !== 'string') {
    return
  }
  const assets = options[type]
  // check local registration variations first
  if (hasOwn(assets, id)) return assets[id]
  const camelizedId = camelize(id) //转换为驼峰式
  if (hasOwn(assets, camelizedId)) return assets[camelizedId]
  const PascalCaseId = capitalize(camelizedId) //将驼峰式的首字母大写
  if (hasOwn(assets, PascalCaseId)) return assets[PascalCaseId]
  // fallback to prototype chain
  //如果在实例上未找到相应属性，则在原型链上查找，
  //注意，在实例化Vue实例时，Vue构造函数及其子构造函数中的options对象中的组件、指令和过滤器
  //都会合并到Vue实例的$options属性中。
  //所以这里查找原型链有什么用吗？因为assets就是一个普通的对象吧？
  //不是的，由当前文件中的mergeAssets方法可知，其合并策略中，就是通过create方法创建继承关系的，
  //所以通过原型可以查找到。
  const res = assets[id] || assets[camelizedId] || assets[PascalCaseId]
  if (process.env.NODE_ENV !== 'production' && warnMissing && !res) {
    warn(
      'Failed to resolve ' + type.slice(0, -1) + ': ' + id,
      options
    )
  }
  return res
}
